package ca.etsmtl.log730.projet2.connectors.clientserver;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.Vector;

import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoHandler;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.serialization.ObjectSerializationCodecFactory;
import org.apache.mina.filter.logging.LoggingFilter;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;
import org.apache.mina.transport.socket.nio.NioSocketConnector;

import ca.etsmtl.log730.projet2.connectors.clientserver.nodes.DocumentMap;
import ca.etsmtl.log730.projet2.connectors.clientserver.nodes.NodeEntry;
import ca.etsmtl.log730.projet2.connectors.clientserver.nodes.SessionNodeMap;
import ca.etsmtl.log730.projet2.connectors.clientserver.setup.ConnectionData;
import ca.etsmtl.log730.projet2.connectors.msg.Confirmation;
import ca.etsmtl.log730.projet2.connectors.msg.Request;
import ca.etsmtl.log730.projet2.connectors.msg.RequestReply;

public class LinkHandler implements IoHandler {

	private SessionNodeMap nodesMap = new SessionNodeMap();
	public SessionNodeMap getNodesMap() {
		return nodesMap;
	}
	private DocumentMap documentMap = new DocumentMap();
	private RequestManager requestManager;

	public LinkHandler() {
		setRequestManager(new RequestManager(nodesMap, documentMap));
	}
	@Override
	public void exceptionCaught(IoSession ios, Throwable o)
			throws Exception {
	}

	@Override
	public void messageReceived(final IoSession ios, Object o) throws Exception {
			if(o instanceof ArrayList<?>) {
				System.out.println("RECEIVED LIST!@");
				connect((ArrayList<NodeEntry>)o);
			} else if(o instanceof NodeEntry) {
				NodeEntry ne = (NodeEntry)o;
							nodesMap.put(ios, ne);
							System.out.println("SEND LIST!");
							send(ios, new ArrayList<NodeEntry>(nodesMap.values()));
			} else if(o instanceof Request) {
				System.out.println("REQUEST RECEIVED! [" + ((Request)o).getOrdinal() + "]");
				requestManager.handleRequest(ios, (Request)o);
			} else if(o instanceof RequestReply) {
				System.out.println("REPLY-REQUEST RECEIVED!");
				requestManager.addReply(((RequestReply)o).getRequest(), nodesMap.get(ios), (RequestReply)o);
			} else if(o instanceof Confirmation) {
				System.out.println("REQUEST-CONFIRM RECEIVED!");
				requestManager.applyConfirmation((Confirmation)o);
			}
	}

	@Override
	public void messageSent(IoSession ios, Object o) throws Exception {
	}

	@Override
	public void sessionClosed(IoSession ios) throws Exception {
		nodesMap.remove(ios);
	}

	@Override
	public void sessionCreated(final IoSession ios) throws Exception {
		nodesMap.put(ios, new NodeEntry());
	
	}

	@Override
	public void sessionIdle(IoSession ios, IdleStatus o) throws Exception {
	}

	@Override
	public void sessionOpened(IoSession ios) throws Exception {
//		nodesMap.put(ios, new NodeEntry());
		send(ios, nodesMap.getCurrentNode());
	}


	private boolean isListening = false;
	public void start() {
		new Thread(new Runnable() {

			@Override
			public void run() {
				NioSocketAcceptor acceptor = new NioSocketAcceptor();
				
				acceptor.getFilterChain().addLast( "logger", new LoggingFilter() );
				acceptor.getFilterChain().addLast( "codec", new ProtocolCodecFilter(new ObjectSerializationCodecFactory()));
				System.out.println("LISTENING ON PORT " + nodesMap.getCurrentNode().getConnectionData().getPort());
				acceptor.setHandler( LinkHandler.this);
				acceptor.getSessionConfig().setReadBufferSize( 2048 );
				acceptor.getSessionConfig().setIdleTime( IdleStatus.BOTH_IDLE, 10 );
				try {
					acceptor.bind( new InetSocketAddress(nodesMap.getCurrentNode().getConnectionData().getPort()) );
					setListening(true);
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			
		}).start();
	}
	
	
	public void connect(ConnectionData ne) {
		if(getConnections().contains(ne)) {
			System.out.println("ALREADY CONNECTED: " + ne.toString());
			return;
		} else if (getNodesMap().getCurrentNode().getConnectionData().equals(ne)) {

			System.out.println("THIS IS MYSELFT: " + ne.toString());
			return;
		}
		NioSocketConnector connector = new NioSocketConnector();

	    // Configure the service.
	    connector.setConnectTimeoutMillis(5000);
	       connector.getFilterChain().addLast("codec",
	                                         new ProtocolCodecFilter(new ObjectSerializationCodecFactory()));
	    connector.getFilterChain().addLast("logger", new LoggingFilter());
	    connector.setHandler(this);

		    IoSession session;
	        try {
	        	System.out.println("CONNECTING ... " + ne.toString());
	        	InetSocketAddress is = new InetSocketAddress(ne.getHostname(), ne.getPort());
				ConnectFuture future = connector.connect(is);
				future.awaitUninterruptibly();
	            session = future.getSession();
	            NodeEntry nn = new NodeEntry();
	            nn.setConnectionData(ne);
	            this.nodesMap.put(session, nn);
	            System.out.println("CONNECTED!");
	        } catch (Exception e) {
	            System.err.println("Failed to connect.");
	            e.printStackTrace();
	        }
	}
	public Collection<ConnectionData> getConnections() {
		synchronized(nodesMap) {
			Vector<ConnectionData> v = new Vector<ConnectionData>();
			for(NodeEntry ne: this.nodesMap.values()) {
				v.add(ne.getConnectionData());
			}
			return v;
		}
	}
	public void connect(Collection<NodeEntry> c) {
	    for (NodeEntry ne: c) {
	    	if(!getNodesMap().values().contains(ne) && !ne.equals(getNodesMap().getCurrentNode())) {
	    		connect(ne.getConnectionData());
	    	}
	    }

	}

	
	
	public void broadcast(final Object o) {
		new Thread(new Runnable() {

			@Override
			public void run() {
				for(Entry<IoSession, NodeEntry> ee: nodesMap.entrySet()) {
					ee.getKey().write(o);
				}
			}
			
		}).start();
	}
	public void send(final IoSession session,final Object o) {
		new Thread(new Runnable() {

			@Override
			public void run() {
				session.write(o);
			}
			
		}).start();
	}
	public RequestManager getRequestManager() {
		return requestManager;
	}
	public void setRequestManager(RequestManager requestManager) {
		this.requestManager = requestManager;
	}
	public void setListening(boolean t) {
		isListening = t;
	}
	public boolean isListening() {
		return isListening;
	}
	public void stop() {
		setListening(false);
	}
}
